﻿#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = System.Object;

namespace AI.Graph.Editor {


    public class NodeCanvas : GUICanvas<AIGraph> {

#region Fields

        private AIController m_controller;
        private AIBehaviour m_aiBehaviour;
        private bool m_drawSimple = true;
        private float m_gridSpacing = 12;
        private bool m_snapNodesOnGrid;
        private bool m_updateList;
        private NodeIterator m_nodeIterator;
        private List<Node> m_hoveredNodes = new List<Node> ();
        private List<Node, AbstractNodeView> m_nodeViewList = new List<Node, AbstractNodeView> ();
        private Stack<Node> m_lastSelectedNodes = new Stack<Node> ();
        private Node[] m_selectedNodes = new Node[0];

#endregion


#region Properties

        public ConnectionPort selectedPort;

        public bool drawSimple {
            get { return m_drawSimple; }
            set { m_drawSimple = value; }
        }

        public bool snapNodesOnGrid {
            get { return m_snapNodesOnGrid; }
            set {
                if (value && !m_snapNodesOnGrid) {
                    if (m_controller != null) {
                        SetSnapPositionOnNodes (m_controller);
                    }
                }

                m_snapNodesOnGrid = value;
            }
        }

        public event Action<Node[]> onNodesSelected;

        public event Action<ConnectionPort, ConnectionPort> onEdgeClicked;

        public float deltaTime {
            get { return window.deltaTime; }
        }

        public NodeIterator nodes {
            get { return m_nodeIterator; }
        }

#endregion


        public NodeCanvas (AIGraph window) : base (window) {
            m_nodeIterator = new NodeIterator (this);
            AIGraph.onControllerChanged -= SetController;
            AIGraph.onControllerChanged += SetController;
        }


        void SetController (AIController controller, AIBehaviour aiBehaviour) {
            Reset ();

            m_controller = controller;
            m_aiBehaviour = aiBehaviour;

            if (m_controller != null) {
                offset = m_controller.offset;
                SetSnapPositionOnNodes (m_controller);
                UpdateNodeList ();

                m_controller.onNodeAdded += node => { UpdateNodeList (); };
                m_controller.onNodeRemoved += node => { UpdateNodeList (); };
            }
        }


        void SetSnapPositionOnNodes (AIController controller) {
            foreach (Node node in controller.nodes) {
                node.position = ConvertToSnapPosition (node.position);
            }
        }


        protected override void OnCanvasGUI () {
            if (Event.current.IsRepaintEvent ()) {
                DrawGrid ();
            }

            if (m_controller == null) {
                return;
            }

            m_controller.offset = offset;

            SwapNodes ();
            DrawConnections ();
            DrawNodes ();

            if (m_updateList) {
                m_updateList = false;
                UpdateNodeListImmediate (m_controller.nodes);
            }
        }


        public override void OnSceneGUI () {
            if (m_aiBehaviour != null) {
                for (int i = m_nodeViewList.length - 1; i >= 0; i--) {
                    if (!m_nodeViewList[i].isHidden) {
                        m_nodeViewList[m_nodeViewList[i]].DrawOnScene ();
                    }
                }
            }
        }


        protected override void OnHandleEvents (Event e) {
            if (e.IsKeyDown (KeyCode.F)) {
                if (m_selectedNodes.Length > 0) {
                    SetZoom (1, ScreenToCanvasPoint (unscaledRect.center));

                    Bounds bounds = new Bounds ();
                    bounds.center = m_selectedNodes[0].rect.center;
                    bounds.size = m_selectedNodes[0].rect.size;

                    for (int i = 1; i < m_selectedNodes.Length; i++) {
                        bounds.Encapsulate (new Bounds (m_selectedNodes[i].rect.center, m_selectedNodes[i].size));
                    }

                    Vector2 nodesCenter = bounds.center;
                    Vector2 canvasCenter = ScreenToCanvasPoint (unscaledRect.center);

                    Vector2 centerOffset = canvasCenter - nodesCenter;

                    offset = offset + centerOffset;
                }
            }
        }


        void DrawGrid () {
            float smallGridOpacity = Mathf.Clamp (zoomPercentage, 0, 0.3F);
            DrawGrid (scaledRect, offset, m_gridSpacing, Styles.GetGridColor (smallGridOpacity));

            float bigGridOpacity = Mathf.Clamp (zoomPercentage, 0.2F, 0.5F);
            DrawGrid (scaledRect, offset, m_gridSpacing * 10, Styles.GetGridColor (bigGridOpacity));
        }


        void DrawGrid (Rect rect, Vector2 offset, float spacing, Color color) {
            int horizontalLineCount = Mathf.CeilToInt (rect.width / spacing) + 1;
            int verticalLineCount = Mathf.CeilToInt (rect.height / spacing) + 1;

            Handles.BeginGUI ();
            Handles.color = color;

            Vector2 lineOffset = -offset + new Vector2 (offset.x % spacing, offset.y % spacing);

            // Vertical lines
            for (int i = 0; i < horizontalLineCount; i++) {
                Vector2 startPoint = new Vector2 (i * spacing + lineOffset.x, -offset.y);
                Vector2 endPoint = startPoint + new Vector2 (0, rect.height);

                Handles.DrawLine (startPoint, endPoint);
            }

            // Horizontal lines
            for (int i = 0; i < verticalLineCount; i++) {
                Vector2 startPoint = new Vector2 (-offset.x, spacing * i + lineOffset.y);
                Vector2 endPoint = startPoint + new Vector2 (rect.width, 0);

                Handles.DrawLine (startPoint, endPoint);
            }

            Handles.color = Color.white;
            Handles.EndGUI ();
        }


        void DrawNodes () {
            // Start from the bottom of the list because of the overdraw order.
            // We want to hover and select the latest drawn node.
            for (int i = m_nodeViewList.length - 1; i >= 0; i--) {
                if (!m_nodeViewList[i].isHidden) {
                    if (IsRectInView (m_nodeViewList[i].rect, 50)) {
                        m_nodeViewList[m_nodeViewList[i]].DrawNode (m_drawSimple);
                    }
                }
            }

            ClearHoveredNodes ();
        }


        void DrawConnections () {
            bool isMouseOverEdge = false;
            bool clickedOnEdge = false;

            for (int i = m_nodeViewList.length - 1; i >= 0; i--) {
                Node node = m_nodeViewList[i];

                if (node.isHidden) continue;

                ConnectionPort[] fromPorts = node.outputs;
                for (int fromPortIndex = 0; fromPortIndex < fromPorts.Length; fromPortIndex++) {
                    ConnectionPort fromPort = fromPorts[fromPortIndex];

                    if (fromPort.isHidden) continue;

                    EdgePoint edgePoint1 = m_nodeViewList[node].GetEdgePointForPort (fromPort);
                    ConnectionPort[] toPorts = fromPort.connectedPorts;

                    for (int toPortIndex = 0; toPortIndex < toPorts.Length; toPortIndex++) {
                        ConnectionPort toPort = toPorts[toPortIndex];
                        if (!m_nodeViewList.ContainsKey (toPort.node)) continue;
                        if (toPort.isHidden) continue;
                        if (toPort.node.isHidden) continue;

                        EdgePoint edgePoint2 = m_nodeViewList[toPort.node].GetEdgePointForPort (toPort);

                        if (IsRectInView (fromPort.rect) || IsRectInView (toPort.rect)) {
                            bool mouseOverEdge = EditorGUI.DrawEdge (edgePoint1, edgePoint2, 3, () => {
                                if (!clickedOnEdge) {
                                    clickedOnEdge = true;
                                    onEdgeClicked?.Invoke (fromPort, toPort);
                                }
                            }, !isMouseOverEdge, 15);

                            if (mouseOverEdge) {
                                isMouseOverEdge = true;
                            }
                        }
                    }
                }
            }

            if (selectedPort) {
                EditorGUI.DrawEdgeToMouse (m_nodeViewList[selectedPort.node].GetEdgePointForPort (selectedPort), Color.white);
            }
        }


        void UpdateNodeList () {
            m_updateList = true;
        }


        void UpdateNodeListImmediate (Node[] nodes) {
            Stack<Node> nodesToRemove = new Stack<Node> ();

            for (int i = 0; i < m_nodeViewList.length; i++) {
                bool stillExists = false;
                for (int j = 0; j < nodes.Length; j++) {
                    if (m_nodeViewList[i] == nodes[j]) {
                        stillExists = true;
                        break;
                    }
                }

                if (!stillExists) {
                    nodesToRemove.Push (m_nodeViewList[i]);
                }
            }

            while (nodesToRemove.Count > 0) {
                m_nodeViewList.Remove (nodesToRemove.Pop ());
            }

            for (int i = 0; i < nodes.Length; i++) {
                if (!m_nodeViewList.ContainsKey (nodes[i])) {
                    AbstractNodeView view = (AbstractNodeView) UnityEditor.Editor.CreateEditor (nodes[i]);
                    if (view) {
                        m_nodeViewList.Add (nodes[i], view);
                        view.Initialize (this, m_aiBehaviour);
                    }
                    else {
                        Debug.Log ("A View for the node [" + nodes[i].name + " could not be created.");
                    }
                }
            }
        }


        void SwapNodes () {
            while (m_lastSelectedNodes.Count > 0) {
                m_nodeViewList.SetFirstItem (m_lastSelectedNodes.Pop ());
            }
        }


        public Vector2 ConvertToSnapPosition (Vector2 position) {
            return new Vector2 (
                Mathf.RoundToInt (position.x / m_gridSpacing) * m_gridSpacing,
                Mathf.RoundToInt (position.y / m_gridSpacing) * m_gridSpacing);
        }


        bool IsRectInView (Rect rect, float epsilon = 0) {
            Vector2 screenMinPos = CanvasToScreenPoint (rect.min);
            Vector2 screenMaxPos = CanvasToScreenPoint (rect.max);
            return screenMaxPos.x > unscaledRect.xMin - epsilon && screenMinPos.x < unscaledRect.xMax + epsilon && screenMaxPos.y > unscaledRect.yMin - epsilon && screenMinPos.y < unscaledRect.yMax + epsilon;
        }


        public bool IsPortSelected (ConnectionPort port) {
            return selectedPort == port;
        }


        public bool IsNodeSelected (Node node) {
            foreach (Node selectedNode in GetSelectedNodes ()) {
                if (selectedNode == node) return true;
            }

            return false;
        }


        public Node[] GetSelectedNodes () {
            return m_selectedNodes;
        }


        public void SetSelectedNode (Node node) {
            SetSelectedNodes (new[] {node});
        }


        public void SetSelectedNodes (Node[] nodes) {
            bool difference = false;
            UnityEngine.Object[] currentObjects = Selection.objects;

            if (currentObjects.Length != nodes.Length) {
                difference = true;
            }
            else {
                for (int i = 0; i < currentObjects.Length; i++) {
                    Object obj = currentObjects[i];

                    if (!obj.IsTypeOf<Node> ()) {
                        difference = true;
                        break;
                    }

                    Node node = obj as Node;
                    bool alreadySelected = false;

                    for (int j = 0; j < nodes.Length; j++) {
                        if (node == nodes[j]) {
                            alreadySelected = true;
                            break;
                        }
                    }

                    if (!alreadySelected) {
                        difference = true;
                        break;
                    }
                }
            }

            if (difference) {
                ArrayUtility.Clear (ref m_selectedNodes);
                ArrayUtility.AddRange (ref m_selectedNodes, nodes);
                Selection.objects = nodes;

                // add all new selected nodes at the first entry's in the sortedList.
                for (int i = 0; i < nodes.Length; i++) {
                    m_lastSelectedNodes.Push (nodes[i]);
                }

                onNodesSelected?.Invoke (GetSelectedNodes ());
            }
        }


        public void ClearSelectedNodes () {
            ArrayUtility.Clear (ref m_selectedNodes);
            Selection.activeObject = null;

            onNodesSelected?.Invoke (new Node[0]);
        }


        public bool IsNodeHovered (Node node) {
            for (int i = 0; i < m_hoveredNodes.Count; i++) {
                if (m_hoveredNodes[i] == node) return true;
            }

            return false;
        }


        public void SetHoveredNode (Node node) {
            ClearHoveredNodes ();
            m_hoveredNodes.Add (node);
        }


        public void SetHoveredNodes (Node[] nodes) {
            for (int i = 0; i < nodes.Length; i++) {
                m_hoveredNodes.Add (nodes[i]);
            }
        }


        void ClearHoveredNodes () {
            m_hoveredNodes.Clear ();
        }


        void Reset () {
            m_hoveredNodes.Clear ();
            m_lastSelectedNodes.Clear ();

            for (int i = 0; i < m_nodeViewList.length; i++) {
                UnityEngine.Object.DestroyImmediate (m_nodeViewList.GetValue (i));
            }

            m_nodeViewList.CLear ();
        }


        // todo unselect current selected nodes.
        public override void OnDisable () {
            Reset ();
        }


        public class NodeIterator {

            private NodeCanvas m_canvas;


            public NodeIterator (NodeCanvas canvas) {
                m_canvas = canvas;
            }


            public int Length {
                get { return m_canvas.m_nodeViewList.length; }
            }

            public Node this [int index] {
                get { return m_canvas.m_nodeViewList[index]; }
            }

        }


        static class Styles {

            private static readonly EditorColor gridColor = new EditorColor (new Color32 (40, 40, 40, 255), new Color32 (0, 0, 0, 255));


            public static Color GetGridColor (float alpha) {
                Color color = gridColor.color;
                color.a = alpha;
                return color;
            }

        }

    }


}
#endif